设计模式之模版方法(Template Method)模式

序言——无纸化系统实现办证流程

  为了适应新时代,国家大力推广,支持各大中小企业打造“无纸化系统”。

  企业使用“无纸化办公”系统,将许多传统工作从线下转为线上,以电子文件替代有纸化文件,不仅能降低企业成本,而且对使用者友好,对自然环境友好,一举多得!

  在以前,人们若想办理证件,大致需要经历以下流程:

  • ① 办证人去办事处(A 地点)领取纸质文件,填写对应内容
  • ② 办证人去对应审核中心(B 地点),提交填写完成的文件
  • ③ 办证人等待审核中心审核:
    • 审核中心审核不通过,从 ① 开始重新走流程
    • 审核中心审核通过,给文件盖章后发放给办证人,进入下一步骤
  • ④ 办证人去证件办理中心(C 地点),提交盖章完毕的文件,办证人等待证件办理完成
  • ⑤ 证件办理中心通知证件已办理完成,发短信告知办证人领取
  • ⑥ 办证人去证件办理中心领取证件

  在此流程下办证,人们不仅要在各个地方来回跑,十分花费时间,而且,若文件有一个地方填错了,还得重新走一遍流程。。。

  而现在,将办理证件的流程信息化,在对应电子化系统中进行操作,那么,一切都变得简单了:

  • ① 办证人在系统中,根据办理证件类型,下载所需填写的电子文件模版
  • ② 办证人根据模版将电子文件填写完成后,上传到办证系统中
  • ③ 办证人等待办证系统审核中心工作人员审核:
    • 审核中心审核不通过,从 ① 开始重新填写上传对应文件走流程
    • 审核中心审核通过,文件自动进行电子盖章,自动进入下一步骤
  • ④ 系统判断所有文件电子盖章无误,告知证件办理中心办理证件
  • ⑤ 证件办理中心通知证件已办理完成,发短信告知办证人领取
  • ⑥ 办证人去证件办理中心领取证件

  通过“无纸化办公”系统,人们只需要在网上动动小手,当流程办理完成后,直接去证件办理中心领取,只需要跑一次,大大节约了时间(另外如果系统支持快递送件,那都不用跑了)。

设计与实现

  对于上面谈论的电子化办证流程,其实是需要程序员根据标准化的文档流程进行抽象设计,最终进行代码实现的,针对这个过程,下面我们分析一下。

  首先,程序员需要开发一个审批流系统,按需求规则去定义一个审批流模版,针对办证的各个步骤,转换为审批流当中的一个个节点。

  另外,审批流系统需要对接其他第三方的系统,因为具体办证的业务是政务机构提供的,而审批流系统并不负责此业务。

  分析下来,那么代码实现上,主要时间花费在两块:

  • ① 审批流系统设计
  • ② 对接第三方系统

  本文不谈论第一点,因为市面上现有的审批流系统非常多,具体根据需要去调研实现即可,那么,第二点如何改如何考量呢?

  由于办证相关业务是由政务机构提供,对审批流系统开发人而言,必须按其他机构提供的接口文档进行对接工作,因为需要连调测试工作,是比较耗费时间的。

流程设计

  当然,我们在进行接口文档对接前,首先应该是梳理下大致的流程步骤,那么,我们画一个流程图出来吧:

办证流程图

  从流程图可知,对接第三方系统,大致需要以下接口:

  • 申请接口
  • 文件上传接口
  • 进度状态查询接口
  • 补充资料相关接口(二次申请接口)

代码实现探秘

  在审批流系统已完成,但是还未对接第三方系统的情况下,我们先思考一下:关于对接第三方系统的代码,应该写在哪里?
  直接嵌入到各个流程节点当中嘛?

  当然不是,我们应当是定义出一个单独的接口服务,提供给审批流调用。

  那么,我们应该会定义一个这样的接口,然后使用相应的实现类去实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface IThirdCardService {

/**
* 申请接口
*
* @param applyParams
*/
void apply(Object applyParams);

/**
* 文件上传接口
*
* @param uploadParams
*/
void upload(Object uploadParams);

/**
* 状态查询接口
*
* @param queryStatusParams
*/
void queryStatus(Object queryStatusParams);

/**
* 二次申请接口
*
* @param reApplyParams
*/
void reApply(Object reApplyParams);

}

  我们这么做会有什么问题吗?

  现在来看,确实是没啥问题的,我们现在对接了 A 办证系统,而 A 办证系统支持办理 A1、A2、A3 等种类的证件。

  但是,客户之后反馈要办只有 B 办证系统支持的 B1、B2、B3 类证件,而 A 办证系统并不支持。

  产品经理说:这肯定不行呀,客户的需求我们得满足呀,开发老弟,麻烦再去接入 B 办证系统吧!

  开发同学:……

  世界上没有两片完全相同的树叶,对 A、B 办证系统同样如此,虽然两个系统大体办证流程差不多,但部分地方还是会存在差异,比如说:

  • ① 接口地址配置需要变化:A、B 办证系统的接口地址一定是不同的
  • ② 接口方法需要变化:B 系统特别增加了文件上传前校验的接口

  开发同学分析了下需求,既然这样,我再写一个原接口的实现类,然后实现一下,这不就是策略模式吗,简单呀!

  哎,这代码怎么看起来不对劲啊?哦,少了个校验接口,emmm,怎么办呢?

  我再去接口里面加个文件校验方法吧。

  嗯?怎么又报错了?哦哦,A 办证系统的实现类没有实现该方法,我再去 A 办证系统的实现类实现下。

  这代码看起来怎么怪怪的,而且,我发现有些代码重复了。

错误的实现

  从前面知道了,现在的代码是有一定问题的:

  • 无法应对变化:办证流程新增一个步骤就需要修改原先每个相关类的代码(接口及实现),不符合开闭原则
  • A 系统并不需要校验文件接口,但是由于接口定义的抽象方法,还是必须在其所有实现类中进行实现,哪怕用不到,与业务冲突

  显而易见,策略模式无法应对这种情况,策略模式关注的是相同行为的抽象,这种情况根本就不应该使用策略模式。

  不过没关系,我们可以使用另一种设计模式——模版方法模式。

优化后的代码

  现在,即使再新增一个 C 办证系统(提供 C1、C2 证件办理),我们也只需要去新增一个模版的实现类去继承模版类即可。

  通过继承,子类可以共用相同的代码,也可以各自实现不同的代码,还可以根据需要,去实现钩子函数(某些相同或不同的代码)。

简介

  模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架,使得子类 可以在不改变一个算法的结构的情况下,重定义该算法的某些特定步骤。

优与劣

优点

  • 钩子函数可以应对变化的行为,子类按业务需要确定是否重写
  • 利用模板将不变的行为(或属性)抽离到父类中,提高了代码的复用性,符合开闭原则
  • 将不同的算法逻辑分离到不同的子类中,通过对子类的扩展增加新的行为,提高了代码的可扩展性

缺点

  • 钩子函数破坏了里氏代换原则
  • 由于继承的缺点,若在父类添加新的抽象方法,则所有子类都需再实现一遍
  • 每一个抽象类都需要一个子类实现,这将导致类数量增加,而类数量的增加,将间接地增加了系统实现的复杂度

业务场景

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  • 各子类中公共的行为需要被提取集中到一个公共父类中,以避免代码重复
  • 子类需要应对扩展,可在模板父类中定义只在特定点调用的钩子函数, 以兼容特殊的业务

参考

  • 谭勇德. 设计模式就该这样学 [M]. 电子工业出版社,2020

文章信息

时间 说明
2019-03-29 初版
2022-07-19 完全重构
0%